{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Streaming Meetups Dashboard"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The purpose of this notebook is to give an all-in-one demo of streaming data from the [meetup.com RSVP API](http://www.meetup.com/meetup_api/docs/stream/2/rsvps/#websockets), through a local [Spark Streaming job](http://spark.apache.org/streaming/), and into [declarative widgets](https://github.com/jupyter-incubator/declarativewidgets) in a dashboard layout. You can peek at the meetup.com stream [here](http://stream.meetup.com/2/rsvps)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This example is a Scala adaptation of [this](https://github.com/jupyter-incubator/dashboards/blob/master/etc/notebooks/stream_demo/meetup-streaming.ipynb) notebook from [jupyter_dashboards](https://github.com/jupyter-incubator/dashboards).\n",
    "\n",
    "On your first visit to this notebook, we recommend that you execute one cell at a time as you read along. Later, if you  just want to see the demo, select *Cell > Run All* from the menu bar. Once you've run all of the cells, select *View > View Dashboard* and then click on the **Stream** toggle to start the data stream.\n",
    "\n",
    "**Table of Contents**\n",
    "\n",
    "1. [Initialize DeclarativeWidgets](#Initialize-DeclarativeWidgets) <span class=\"text-muted\" style=\"float:right\"></span>\n",
    "2. [Define the Spark Streaming Job](#Streaming-Meetups-Scala) <span style=\"float:right\" class=\"text-muted\">create stream context, custom receiver, filter by topic, top topics, venue metadata</span>\n",
    "3. [Create a Dashboard Interface with Widgets](#Streaming-Meetups-Dashboard)<span style=\"float:right\" class=\"text-muted\">charts, interactive controls, globe</span>\n",
    "4. [Arrange the Dashboard Layout](#Arrange-the-Dashboard-Layout-Top)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<div class=\"alert alert-info\" role=\"alert\" style=\"margin-top: 10px\">\n",
    "<p><strong>Note</strong><p>\n",
    "\n",
    "<p>We've condensed all of the demo logic into a single notebook for educational purposes. If you want to turn this into a scalable, multi-tenant dashboard, you'll want to separate the stream processing portions from the dashboard view. That way, multiple dashboard instances can pull from the same processed data stream instead of recomputing it.</p>\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Initialize DeclarativeWidgets<span style=\"float: right; font-size: 0.5em\"><a href=\"#Initialize-DeclarativeWidgets\">Top</a></span>\n",
    "\n",
    "In Toree, declarativewidgets need to be initialized by adding the JAR with the scala implementation and calling `initWidgets`. This is must take place very close to the top of the notebook."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "%%html\n",
    "<span id=\"__jar_url__\"></span>\n",
    "<script>\n",
    "(function(){\n",
    "    var thisUrl = this.location.toString();\n",
    "    var jarUrl = thisUrl.substring(0,thisUrl.indexOf(\"/notebooks\"))+\"/nbextensions/urth_widgets/urth-widgets.jar\";\n",
    "    document.getElementById(\"__jar_url__\").innerHTML = jarUrl;\n",
    "})();\n",
    "</script>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<div class=\"alert alert-info\" role=\"alert\" style=\"margin-top: 10px\">\n",
    "<p><strong>Note</strong> The output of the cell above is the URL to use below.\n",
    "</div>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "%AddJar http://localhost:8888/nbextensions/urth_widgets/urth-widgets.jar"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "import urth.widgets._\n",
    "initWidgets"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Define the Spark Streaming Application<span style=\"float: right; font-size: 0.5em\"><a href=\"#Streaming-Meetups-Scala\">Top</a></span>\n",
    "\n",
    "With the frontend widgest in mind, we'll now setup our Spark Streaming job to fulfill their data requirements. In this section, we'll define a set of functions that act on a `SparkStreamingContext` or `RDDs` from that context."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Install external dependencies"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "%AddDeps eu.piotrbuda scalawebsocket_2.10 0.1.1 --transitive"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Custom WebSocker Receiver\n",
    "\n",
    "We create here a custom receiver that can connect to a WebSocket. That is how we will stream data out of the Meetup API."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "import scalawebsocket.WebSocket\n",
    "import org.apache.spark.storage.StorageLevel    \n",
    "import org.apache.spark.streaming.receiver.Receiver\n",
    "\n",
    "class WebSocketReceiver(url: String, storageLevel: StorageLevel = StorageLevel.MEMORY_ONLY_SER) extends Receiver[play.api.libs.json.JsValue](storageLevel) {\n",
    "    @volatile private var webSocket: WebSocket = _\n",
    "\n",
    "    def onStart() {\n",
    "        try{\n",
    "          val newWebSocket = WebSocket().open(url).onTextMessage({ msg: String => parseJson(msg) })\n",
    "          setWebSocket(newWebSocket)\n",
    "        } catch {\n",
    "          case e: Exception => restart(\"Error starting WebSocket stream\", e)\n",
    "        }\n",
    "    }\n",
    "\n",
    "    def onStop() {\n",
    "        setWebSocket(null)\n",
    "    }\n",
    "\n",
    "    private def setWebSocket(newWebSocket: WebSocket) = synchronized {\n",
    "        if (webSocket != null) {\n",
    "          webSocket.shutdown()\n",
    "        }\n",
    "        webSocket = newWebSocket\n",
    "    }\n",
    "\n",
    "    private def parseJson(jsonStr: String): Unit = {\n",
    "        val json: play.api.libs.json.JsValue = play.api.libs.json.Json.parse(jsonStr)\n",
    "        store(json)\n",
    "    }\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Spark Meetup Application\n",
    "\n",
    "We then put all our functionality into an `object` that is marked as `Serializable`. This is a great technique to avoid serialization problems when interacting with Spark. Also not that anything that we do not want to serialize, such as the `StreamingContext` and the `SQLContext` should be marked `@transient`.\n",
    "\n",
    "The rest of the call are methods for starting and stopping the streaming application as well as functions that define the streaming flow."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "case class TopicCount(topic: String, count: Int)\n",
    "\n",
    "object MeetupApp extends Serializable {    \n",
    "    import play.api.libs.json._\n",
    "    import org.apache.spark.storage.StorageLevel    \n",
    "    import org.apache.spark.Logging\n",
    "    import org.apache.spark.streaming._\n",
    "    import org.apache.spark.sql.functions._\n",
    "    import org.apache.spark.streaming.dstream.DStream\n",
    "    import org.apache.spark.rdd.RDD\n",
    "    import urth.widgets.WidgetChannels.channel\n",
    "    import org.apache.spark.sql.SQLContext\n",
    "    \n",
    "    @transient var ssc:StreamingContext = _\n",
    "    @transient val sqlContext:SQLContext = new SQLContext(sc)\n",
    "    \n",
    "    import sqlContext.implicits._\n",
    "    \n",
    "    var topic_filter = \"\"\n",
    "    \n",
    "    //Resetting the values on the data channels\n",
    "    channel(\"status\").set(\"streaming\", false)\n",
    "    channel(\"stats\").set(\"topics\", Map())\n",
    "    channel(\"stats\").set(\"venues\", List())\n",
    "\n",
    "    def create_streaming_context(sample_rate: Int): StreamingContext = {\n",
    "        return new StreamingContext(sc, Seconds(sample_rate))\n",
    "    }\n",
    "    \n",
    "    /**\n",
    "        Creates a websocket client that pumps events into a ring buffer queue. Creates\n",
    "        a SparkStreamContext that reads from the queue. Creates the events, topics, and\n",
    "        venues DStreams, setting the widget channel publishing functions to iterate over\n",
    "        RDDs in each. Starts the stream processing.\n",
    "    */\n",
    "    def start_stream():Unit = {\n",
    "        ssc = create_streaming_context(5)\n",
    "        ssc.checkpoint(\"/tmp/meetups.checkpoint\")\n",
    "        val events = get_events(ssc, sample_event)\n",
    "        get_topics(events, get_topic_counts)\n",
    "        get_venues(events, aggregate_venues)\n",
    "        ssc.start()\n",
    "        \n",
    "        channel(\"status\").set(\"streaming\", true)\n",
    "    }\n",
    "\n",
    "    /**\n",
    "        Shuts down the websocket, stops the streaming context, and cleans up the file ring.\n",
    "    */\n",
    "    def shutdown_stream():Unit = {\n",
    "        ssc.stop(false)\n",
    "        channel(\"status\").set(\"streaming\", false)        \n",
    "    }\n",
    "    \n",
    "    /**\n",
    "    Parses the events from the queue. Retains only those events that have at\n",
    "    least one topic exactly matching the current topic_filter. Sends event\n",
    "    RDDs to the for_each function. Returns the event DStream.\n",
    "    */\n",
    "    def get_events(ssc: StreamingContext, for_each: (RDD[play.api.libs.json.JsValue]) => Unit): org.apache.spark.streaming.dstream.DStream[play.api.libs.json.JsValue] = {\n",
    "\n",
    "        val  all_events = ssc.receiverStream( new WebSocketReceiver(\"ws://stream.meetup.com/2/rsvps\"))\n",
    "\n",
    "        // Filter set of event\n",
    "        val events = all_events.filter(retain_event)\n",
    "\n",
    "        // Send event data to a widget channel. This will be covered below.\n",
    "        events.foreachRDD(for_each)\n",
    "\n",
    "        return events\n",
    "    }\n",
    "    \n",
    "    /**\n",
    "    Returns true if the user defined topic filter is blank or if at least one\n",
    "    group topic in the event exactly matches the user topic filter string.\n",
    "    */\n",
    "    def retain_event(event: play.api.libs.json.JsValue):Boolean = {\n",
    "        val topics = (event \\ \"group\" \\ \"group_topics\").as[play.api.libs.json.JsArray].value\n",
    "        val isEmpty = topic_filter.trim == \"\"\n",
    "        val containsTopic =  topics.map(topic => topic_filter.\n",
    "                                   equals((topic\\\"urlkey\").as[play.api.libs.json.JsString].value)\n",
    "                                ).reduce( (a,b) => a || b)\n",
    "\n",
    "        isEmpty || containsTopic   \n",
    "    }\n",
    "    \n",
    "    /*\n",
    "    Takes an RDD from the event DStream. Takes one event from the RDD.\n",
    "    Substitutes a placeholder photo if the member who RSVPed does not\n",
    "    have one. Publishes the event metadata on the meetup channel.\n",
    "    */\n",
    "    def sample_event(rdd: org.apache.spark.rdd.RDD[play.api.libs.json.JsValue]):Unit = {\n",
    "        \n",
    "        try {\n",
    "            val event = rdd.take(1)(0)\n",
    "\n",
    "            // use a fallback photo for those members without one\n",
    "            val default_event: play.api.libs.json.JsObject = play.api.libs.json.Json.parse(\"\"\"{\n",
    "                 \"member\" : {\n",
    "                     \"photo\" : \"http://photos4.meetupstatic.com/img/noPhoto_50.png\"\n",
    "                 }\n",
    "             }\n",
    "            \"\"\").as[play.api.libs.json.JsObject]\n",
    "            val fixed_event = default_event ++ (event).as[play.api.libs.json.JsObject] \n",
    "\n",
    "            channel(\"meetups\").set(\"meetup\", fixed_event.value)\n",
    "        } catch {\n",
    "          case _ => print(\"No data to sample\")\n",
    "        }\n",
    "    }\n",
    "    \n",
    "    /**\n",
    "    Pulls group topics from meetup events. Counts each one once and updates\n",
    "    the global topic counts seen since stream start. Sends topic count RDDs\n",
    "    to the for_each function. Returns nothing new.\n",
    "    */    \n",
    "    def get_topics(events:DStream[JsValue], for_each: (RDD[((String,String),Int)]) => Unit) = {\n",
    "        //Extract the group topic url keys and \"namespace\" them with the current topic filter\n",
    "        val topics = events.\n",
    "                        flatMap( (event: JsValue) => {\n",
    "                          (event \\ \"group\" \\ \"group_topics\").as[JsArray].value\n",
    "                        }).map((topic: JsValue) => {\n",
    "                            val filter = if(topic_filter.equals(\"\")) {\n",
    "                                \"*\"\n",
    "                            } else { \n",
    "                                topic_filter \n",
    "                            }\n",
    "                            ((filter, (topic \\ \"urlkey\").as[JsString].value), 1)      \n",
    "                        })\n",
    "\n",
    "        val topic_counts = topics.updateStateByKey(update_topic_counts)\n",
    "\n",
    "        // Send topic data to a widget channel. This will be covered below.\n",
    "        topic_counts.foreachRDD(for_each)\n",
    "    }\n",
    "    \n",
    "    /**\n",
    "    Sums the number of times a topic has been seen in the current sampling\n",
    "    window. Then adds that to the number of times the topic has been\n",
    "    seen in the past. Returns the new sum.\n",
    "    */\n",
    "    def update_topic_counts(new_values: Seq[Int], last_sum: Option[Int]): Option[Int] = {\n",
    "        return Some((new_values :+ 0).reduce(_+_) + last_sum.getOrElse(0))\n",
    "    }\n",
    "        \n",
    "    /**\n",
    "    Takes an RDD from the topic DStream. Takes the top 25 topics by occurrence\n",
    "    and publishes them in a pandas DataFrame on the counts channel.\n",
    "    */\n",
    "    def get_topic_counts(rdd: RDD[((String,String),Int)]){\n",
    "        //counts = rdd.takeOrdered(25, key=lambda x: -x[1])\n",
    "        val filterStr = if (topic_filter.equals(\"\")) \"*\" else topic_filter\n",
    "        \n",
    "        /*\n",
    "         keep only those matching current filter\n",
    "         and sort in descending order, taking top 25\n",
    "        */\n",
    "        val countDF = rdd.filter((x:((String,String),Int)) => filterStr.equals(x._1._1)).\n",
    "                    map(tuple => TopicCount(tuple._1._2, tuple._2)).toDF().\n",
    "                    sort($\"count\".desc).limit(25)\n",
    "                        \n",
    "        channel(\"stats\").set(\"topics\", countDF)\n",
    "\n",
    "    }\n",
    "    \n",
    "    /**\n",
    "    Pulls venu metadata from meetup events if it exists. Sends venue \n",
    "    dictionaries RDDs to the for_each function. Returns nothing new.\n",
    "    */\n",
    "    def get_venues(events:DStream[JsValue], calculate: (DStream[JsValue]) => Unit):Unit = {\n",
    "        \n",
    "        val venues = events.\n",
    "            filter(hasVenue).\n",
    "            map((event:JsValue) => {event \\ \"venue\"})\n",
    "\n",
    "        calculate(venues)\n",
    "    }\n",
    "    \n",
    "    /**\n",
    "    Checks if there is a venue in the JSON object\n",
    "    */\n",
    "    def hasVenue(event:JsValue):Boolean = {\n",
    "        (event \\ \"venue\") match {\n",
    "            case e:play.api.libs.json.JsUndefined => false\n",
    "            case _ => true\n",
    "        }\n",
    "    }\n",
    "    \n",
    "    /**\n",
    "    Aggregating the venues by lat/lon\n",
    "    */\n",
    "    def aggregate_venues(venues:DStream[JsValue]): Unit = {\n",
    "        //build a (lat, long, mag) tuple\n",
    "        val venue_loc = venues.map((venue:JsValue) => {\n",
    "            ( \n",
    "                (venue \\ \"lat\").asOpt[Int].getOrElse(0),\n",
    "                (venue \\ \"lon\").asOpt[Int].getOrElse(0)\n",
    "            )\n",
    "        }).\n",
    "        filter((latLon:(Int,Int)) => {latLon != (0,0)}).\n",
    "        map((latLon:(Int,Int)) => {\n",
    "               ((\"\"+latLon._1+\",\"+latLon._2), (latLon._1, latLon._2, 1))\n",
    "        })\n",
    "        \n",
    "        val venue_loc_counts = venue_loc.updateStateByKey(update_venue_loc_counts)\n",
    "        \n",
    "        venue_loc_counts.map{\n",
    "            _._2\n",
    "        }.foreachRDD((rdd:RDD[(Int,Int,Int)]) => {\n",
    "            val venue_data = rdd.collect()\n",
    "            \n",
    "            if( !venue_data.isEmpty ){\n",
    "                val total = venue_data.reduce((a,b)=> (0, 0, a._3+b._3))._3 * 1.0\n",
    "                \n",
    "                val venue_data_list= venue_data.map( (x:(Int,Int,Int)) => List(x._1,x._2,x._3/total)).toList\n",
    "                \n",
    "                channel(\"stats\").set(\"venues\", venue_data_list)\n",
    "            }\n",
    "        })\n",
    "    }\n",
    "    \n",
    "    /**\n",
    "    Function to count the lat, lon\n",
    "    */\n",
    "    def update_venue_loc_counts(new_values: Seq[(Int,Int,Int)], last_sum: Option[(Int,Int,Int)]): Option[(Int,Int,Int)] = {\n",
    "        if( new_values.isEmpty )\n",
    "            last_sum\n",
    "        else{\n",
    "            val partial = new_values.reduce((a,b)=> (a._1, a._2, a._3+b._3))\n",
    "            Some((partial._1, partial._2, partial._3 + last_sum.getOrElse((0,0,0))._3))\n",
    "        }\n",
    "    }\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Create a Dashboard Interface with Widgets <span style=\"float: right; font-size: 0.5em\"><a href=\"#Streaming-Meetups-Dashboard\">Top</a></span>\n",
    "\n",
    "Now that we have a streaming job defined, we can create a series of interactive areas that can control and display the data is being produced."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Control to start and stop the Stream\n",
    "\n",
    "Here we will use a switch widget and tie it to a function that can start and stop the stream job."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "val start_stream = () => {\n",
    "    MeetupApp.start_stream()\n",
    "}:Unit\n",
    "\n",
    "val shutdown_stream = () => {\n",
    "    MeetupApp.shutdown_stream()\n",
    "}:Unit"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "%%html\n",
    "<link rel=\"import\" href=\"urth_components/paper-toggle-button/paper-toggle-button.html\"\n",
    "    is=\"urth-core-import\" package=\"PolymerElements/paper-toggle-button\">\n",
    "    \n",
    "<template is=\"urth-core-bind\" channel=\"status\">\n",
    "    <urth-core-function id=\"start\" ref=\"start_stream\"></urth-core-function>    \n",
    "    <urth-core-function id=\"shutdown\" ref=\"shutdown_stream\"></urth-core-function>    \n",
    "    <paper-toggle-button checked=\"{{streaming}}\" onChange=\"this.checked ? start.invoke() : shutdown.invoke()\">Stream</paper-toggle-button>\n",
    "</template>\n",
    "\n",
    "<style is=\"custom-style\">\n",
    "    paper-toggle-button {\n",
    "        --default-primary-color: green;\n",
    "    }\n",
    "</style>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Topic Bar Chart\n",
    "\n",
    "Here we insert a `<urth-viz-chart>` to show the top 25 meetup topics by occurrence in the stream. Take note of the `<template>` element. We use it to specify that the HTML within will make use of a `counts` channel. We will put data on the `counts` channel later in this notebook."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "%%html\n",
    "<link rel=\"import\" href=\"urth_components/urth-viz-chart/urth-viz-chart.html\" is=\"urth-core-import\">\n",
    "\n",
    "<template is=\"urth-core-bind\" channel=\"stats\">\n",
    "    <urth-viz-chart type='bar' datarows='[[topics.data]]' columns='[[topics.columns]]' rotatelabels='30'></urth-viz-chart>\n",
    "</template>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Topic Filter\n",
    "\n",
    "Next we create an `<urth-core-function>` which that binds the value of a `<paper-input>` widget to a Python function that sets a global variable. The function will set a string that we'll use to filter the incoming events to only pertaining to a certain topic.\n",
    "\n",
    "Notice that the `<link>` tag here is different than what we specified above. `<urth-viz-chart>` is already loaded within the notebook, but here we are using a third-party [Polymer](https://www.polymer-project.org/1.0/) element which needs to download first. To handle that automatically, we specify `is=\"urth-core-import\"` and set the [bower](http://bower.io/) package name as the `package` attribute value."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "val set_topic_filter = (value: String) => {\n",
    "    MeetupApp.topic_filter = value\n",
    "}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "%%html\n",
    "<link rel=\"import\" href=\"urth_components/paper-input/paper-input.html\"\n",
    "    is=\"urth-core-import\" package=\"PolymerElements/paper-input\">\n",
    "    \n",
    "<template is=\"urth-core-bind\" channel=\"filter\" id=\"filter-input\">\n",
    "    <urth-core-function auto\n",
    "        id=\"set_topic_filter\"\n",
    "        ref=\"set_topic_filter\"\n",
    "        arg-value=\"{{topic_filter}}\">\n",
    "    </urth-core-function>\n",
    "        \n",
    "    <paper-input label=\"Filter\" value=\"{{topic_filter}}\"></paper-input>\n",
    "</template>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### User Card\n",
    "\n",
    "Now we add a simple `<paper-card>` element showing the name and photo of one user who RSVPed recently in the event stream. We add some custom styling and a bit of custom JavaScript in this case to format the datetime associated with the RSVP event."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "%%html\n",
    "<link rel=\"import\" href=\"urth_components/paper-card/paper-card.html\"\n",
    "    is=\"urth-core-import\" package=\"PolymerElements/paper-card\">\n",
    "\n",
    "<style is=\"custom-style\">\n",
    "    paper-card.meetups-card {\n",
    "        max-width: 400px;\n",
    "        width: 100%;\n",
    "        \n",
    "        --paper-card-header: {\n",
    "            height: 100px;\n",
    "            border-bottom: 1px solid #e8e8e8;\n",
    "        };\n",
    "\n",
    "        --paper-card-header-image: {\n",
    "            height: 80px;\n",
    "            width: 80px !important;\n",
    "            margin: 10px;\n",
    "            border-radius: 50px;\n",
    "            width: auto;\n",
    "            border: 10px solid white;\n",
    "            box-shadow: 0 0 1px 1px #e8e8e8;\n",
    "        };\n",
    "        \n",
    "        --paper-card-header-image-text: {\n",
    "            left: auto;\n",
    "            right: 0px;\n",
    "            width: calc(100% - 130px);\n",
    "            text-align: right;\n",
    "            text-overflow: ellipsis;\n",
    "            overflow: hidden;\n",
    "        };\n",
    "    }\n",
    "    \n",
    "    .meetups-card .card-content a {\n",
    "        display: block;\n",
    "        overflow: hidden;\n",
    "        text-overflow: ellipsis;\n",
    "        white-space: nowrap;\n",
    "    }\n",
    "</style>\n",
    "\n",
    "<template is=\"urth-core-bind\" channel=\"meetups\" id=\"meetup-card\">\n",
    "    <paper-card\n",
    "            class=\"meetups-card\"\n",
    "            heading=\"[[meetup.member.member_name]]\"\n",
    "            image=\"[[meetup.member.photo]]\">\n",
    "        <div class=\"card-content\">\n",
    "            <p><a href=\"[[meetup.event.event_url]]\" target=\"_blank\">[[meetup.event.event_name]]</a></p>\n",
    "            <p>[[getPrettyTime(meetup.event.time)]]</p>\n",
    "        </div>\n",
    "    </paper-card>\n",
    "</template>\n",
    "\n",
    "<!-- see https://github.com/PolymerElements/iron-validator-behavior/blob/master/demo/index.html -->\n",
    "<script>\n",
    "    (function() {\n",
    "        var dateStringOptions = {weekday:'long', year:'numeric', month: 'long', hour:'2-digit', minute:'2-digit', day:'numeric'};\n",
    "        var locale = navigator.language || navigator.browserLanguage || navigator.systemLanguage || navigator.userLanguage;\n",
    "\n",
    "        var scope = document.querySelector('template#meetup-card');\n",
    "        scope.getPrettyTime = function(timestamp) {\n",
    "            try {\n",
    "                console.log('The date is', timestamp)\n",
    "                var d = new Date(timestamp);\n",
    "                return d.toLocaleDateString(locale, dateStringOptions);\n",
    "            } catch(e){\n",
    "                return ''\n",
    "            }\n",
    "        }\n",
    "    })();\n",
    "</script>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Map Venues\n",
    "\n",
    "Finally, we add a [WebGL globe](https://github.com/dataarts/webgl-globe) showing the location of meetup venues to which users are RSVPing in the stream. On the globe we render bars to represent the number of recent RSVPs in a geographic area."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "%%html\n",
    "<link rel=\"import\" href=\"urth_components/webgl-globe/webgl-globe.html\"\n",
    "  is=\"urth-core-import\" package=\"http://github.com/ibm-et/webgl-globe.git\">\n",
    "\n",
    "<template is=\"urth-core-bind\" channel=\"stats\">\n",
    "    <webgl-globe data=[[venues]]></webgl-globe>\n",
    "</template>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Arrange the Dashboard Layout <span style=\"float: right; font-size: 0.5em\"><a href=\"#Streaming-Meetups-Dashboard\">Top</a></span>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Before toggling the stream on/off switch, we should switch to dashboard view. Otherwise, we'll need to scroll up and down this notebook to see the widgets updating. For convenience, this notebook already contains metadata to position our widgets in a grid layout.\n",
    "\n",
    "Select *View > View Dashboard* from the menu bar to see the dashboard view now. Then toggle the stream switch in the top right of the dashboard to begin stream processing. To return to the regular notebook view, select *View > Notebook*.\n",
    "\n",
    "If you want to arrange the notebook cells differently, select *View > Layout Dashboard*. Then, hover your mouse over the main notebook / dashboard area. When you do, you'll see icons appear that allow you to:\n",
    "\n",
    "- Drag cells to new locations\n",
    "- Resize cells\n",
    "- Show / hide cells in the dashboard view\n",
    "- Flip to editing mode for a cell\n",
    "\n",
    "Save the notebook to save your changes to the layout within the notebook file itself.\n",
    "\n",
    "<div class=\"alert alert-info\" role=\"alert\" style=\"margin-top: 10px\">\n",
    "<p><strong>Note</strong><p>\n",
    "\n",
    "<p>in a fresh notebook, the dashboard will only show cells with non-empty output. All other cells can be found in the *Hidden* section at the bottom of the dashboard layout page. You can quickly add all cell outputs or remove all cell outputs from the dashboard using the show / hide icons that appear in the notebook toolbar when you are in layout mode.</p>\n",
    "</div>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    ""
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Toree - Scala",
   "language": "scala",
   "name": "toree_scala"
  },
  "language_info": {
   "name": "scala"
  },
  "urth": {
   "dashboard": {
    "cellMargin": 10.0,
    "defaultCellHeight": 20.0,
    "layoutStrategy": "packed",
    "maxColumns": 12.0
   }
  }
 },
 "nbformat": 4,
 "nbformat_minor": 0
}